package org.com.net;

import java.io.*;
import java.net.*;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Useful methods for networking stuff.
 *
 * @author See <a href="the Google Code page of the easy-utils
 * library">http://code.google.com/p/easy-utils/</a> for collaborators and other
 * information.
 */
public abstract class UDP {

    /**
     * Sends a packet by using UDP. The size of the buffer for the packet is
     * taken as biggest as possible (65535). Therefore, it is strongly
     * recommended that the way to receive the object is through one of methods
     * intended for it that can be found in this library.
     *
     * @param packet {@link DatagramPacket} The packet to send.
     */
    public static void UDPSendPacket(DatagramPacket packet) {
        UDP.UDPSend(packet.getData(), packet.getAddress(), packet.getPort());
    }

    /**
     * Sends a serializable object by using UDP. The size of the buffer for the
     * packet is taken as biggest as possible (65535). Therefore, it is strongly
     * recommended that the way to receive the object is through one of methods
     * intended for it that can be found in this library.
     *
     * @param object The object to send. Must be serializable.
     * @param destinationAddress {@link InetAddress} The address of the
     * destination.
     * @param port An integer representing the port to use.
     */
    public static void UDPSend(Serializable object, InetAddress destinationAddress, int port) {

        try (DatagramSocket singleSocket = new DatagramSocket(port); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutput out = new ObjectOutputStream(bos)) {
            byte[] buf;
            out.writeObject(object);
            buf = bos.toByteArray();
            DatagramPacket packet = new DatagramPacket(buf, buf.length, destinationAddress, port);
            singleSocket.send(packet);
        } catch (IOException ex) {
            Logger.getLogger(UDP.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Sends a packet of data to a group by using UDP. The size of the buffer
     * for the packet is taken as biggest as possible (65535). Therefore, it is
     * strongly recommended that the way to receive the object is through one of
     * methods intended for it that can be found in this library.
     *
     * @param packet {@link DatagramPacket} The packet to send.
     */
    public static void UDPMultiSendPacket(DatagramPacket packet) {
        UDP.UDPMultiSend(packet.getData(), packet.getAddress(), packet.getPort());
    }

    /**
     * Sends a serializable object to a group by using UDP. The size of the
     * buffer for the packet is taken as biggest as possible (65535). Therefore,
     * it is strongly recommended that the way to receive the object is through
     * one of methods intended for it that can be found in this library.
     *
     * @param object [@link Serializable} The object to send.
     * @param groupAddress {@link String} The address of the group.
     * @param port An integer representing the port to use.
     */
    public static void UDPMultiSend(Serializable object, String groupAddress, int port) {
        UDP.UDPMultiSend(object, UDP.getInetAddressByString(groupAddress), port);
    }

    /**
     * Sends a serializable object to a group by using UDP. The size of the
     * buffer for the packet is taken as biggest as possible (65535). Therefore,
     * it is strongly recommended that the way to receive the object is through
     * one of methods intended for it that can be found in this library.
     *
     * @param object [@link Serializable} The object to send.
     * @param groupAddress {@link InetAddress} The address of the group.
     * @param port An integer representing the port to use.
     */
    public static void UDPMultiSend(Serializable object, InetAddress groupAddress, int port) {
        try (MulticastSocket multiCastSocket = new MulticastSocket(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutput out = new ObjectOutputStream(bos)) {
            byte[] buf;
            out.writeObject(object);
            buf = bos.toByteArray();
            DatagramPacket packet = new DatagramPacket(buf, buf.length, groupAddress, port);
            multiCastSocket.send(packet);
        } catch (IOException ex) {
            Logger.getLogger(UDP.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Receives a serializable object sent by using UDP. The size of the buffer
     * for the packet is taken as biggest as possible (65535). Therefore, it is
     * strongly recommended that the way to send the object is through this
     * library method, {@link BasicNetworking#UDPSendPacket(java.io.Serializable, java.net.InetAddress, int)
     * }.
     *
     * @param port An integer representing the port through trough the which the
     * object must be received.
     * @return {@link Serializable} The received object.
     */
    public static Serializable UDPReceive(int port) {
        Serializable object;

        try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(UDP.UDPReceivePacket(port).getData()))) {
            /*
             * The original object class is restored. The class must be exactly
             * the same than the original one. Otherwise, a {@link
             * ClassNotFoundException} will be thrown.
             */
            object = (Serializable) in.readObject();
        } catch (IOException | ClassNotFoundException ex) {
            Logger.getLogger(UDP.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
        return object;
    }

    /**
     * Receives a packet sent by using UDP. The size of the buffer for the
     * packet is taken as biggest as possible (65535). Therefore, it is strongly
     * recommended that the way to send the object is through this library
     * method, {@link BasicNetworking#UDPSendPacket(java.net.DatagramPacket)}.
     *
     * @param port An integer representing the port through trough the which the
     * packet must be received.
     * @return {@link DatagramPacket} The received packet, or
     * <value>null</value> if any error happens.
     */
    public static DatagramPacket UDPReceivePacket(int port) {
        try (DatagramSocket clientSocket = new DatagramSocket(port)) {
            byte[] receiveData = new byte[65535];
            DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
            clientSocket.receive(receivePacket);
            return receivePacket;
        } catch (IOException ex) {
            Logger.getLogger(UDP.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
    }

    /**
     * Receives a serializable object within a given group sent by using UDP.
     * The size of the buffer for the object is taken as biggest as possible
     * (65535). Therefore, it is strongly recommended that the way to send the
     * object is through one of methods intended for it that can be found in
     * this library.
     *
     * @param port An integer representing the port through trough the which the
     * object must be received.
     * @param groupAddr {@link String} The address of the group.
     * @return {@link Serializable} The received serializable object.
     */
    public static Serializable UDPReceiveSerializableFromGroup(int port, String groupAddr) {
        return UDP.UDPReceiveSerializableFromGroup(port, UDP.getInetAddressByString(groupAddr));
    }

    /**
     * Receives a serializable object within a given group sent by using UDP.
     * The size of the buffer for the object is taken as biggest as possible
     * (65535). Therefore, it is strongly recommended that the way to send the
     * object is through one of methods intended for it that can be found in
     * this library.
     *
     * @param port An integer representing the port through trough the which the
     * object must be received.
     * @param groupAddr {@link InetAddress} The address of the group.
     * @return {@link Serializable} The received serializable object.
     */
    public static Serializable UDPReceiveSerializableFromGroup(int port, InetAddress groupAddr) {
        Serializable object;
        try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(UDP.UDPReceivePacketFromGroup(port, groupAddr).getData()))) {
            /*
             * The original object class is restored. The class must be exactly
             * the same than the original one. Otherwise, a {@link
             * ClassNotFoundException} will be thrown.
             */
            object = (Serializable) in.readObject();
        } catch (IOException | ClassNotFoundException ex) {
            Logger.getLogger(UDP.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
        return object;
    }

    /**
     * Receives a packet within a given group sent by using UDP. The size of the
     * buffer for the object is taken as biggest as possible (65535). Therefore,
     * it is strongly recommended that the way to send the object is through one
     * of methods intended for it that can be found in this library.
     *
     * @param port An integer representing the port through trough the which the
     * object must be received.
     * @param groupAddr {@link InetAddress} The address of the group.
     * @return The received serializable object.
     */
    public static DatagramPacket UDPReceivePacketFromGroup(int port, String groupAddr) {
        return UDP.UDPReceivePacketFromGroup(port, UDP.getInetAddressByString(groupAddr));
    }

    /**
     * Receives a packet within a given group sent by using UDP. The size of the
     * buffer for the object is taken as biggest as possible (65535). Therefore,
     * it is strongly recommended that the way to send the object is through one
     * of methods intended for it that can be found in this library.
     *
     * @param port An integer representing the port through trough the which the
     * packet must be received.
     * @param groupAddr {@link InetAddress} The address of the group.
     * @return {@link DatagramPacket} The received packet.
     */
    public static DatagramPacket UDPReceivePacketFromGroup(int port, InetAddress groupAddr) {
        try (MulticastSocket multiCastSocket = new MulticastSocket(port)) {
            multiCastSocket.joinGroup(groupAddr);
            byte[] data = new byte[65535];
            DatagramPacket packet = new DatagramPacket(data, data.length);
            multiCastSocket.receive(packet);
            multiCastSocket.leaveGroup(groupAddr);
            return packet;
        } catch (IOException ex) {
            Logger.getLogger(UDP.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
    }

    /**
     * Gets the IPv4 address represented by a String.
     *
     * @param address {@link String} The representation of the address.
     * @return {@link InetAddress} The corresponding InetAddress object, or
     * <value>null</value> if an unexpected exception is thrown.
     */
    private static InetAddress getInetAddressByString(String address) {
        int[] components = new int[4];
        int arrayIndex = 0;
        InetAddress addr = null;

        StringTokenizer tokenizer = new StringTokenizer(address, ".");
        while (tokenizer.hasMoreElements()) {
            components[arrayIndex] = Integer.parseInt(tokenizer.nextToken());
            arrayIndex++;
        }

        try {
            addr = InetAddress.getByAddress(new byte[]{(byte) components[0], (byte) components[1], (byte) components[2], (byte) components[3]});
        } catch (UnknownHostException ex) {
            Logger.getLogger(UDP.class.getName()).log(Level.SEVERE, null, ex);
        }

        return addr;
    }
}
